package drz.oddb.view

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.animation.AccelerateDecelerateInterpolator
import drz.oddb.Memory.TupleList
import drz.oddb.R
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.runBlocking
import java.io.File
import java.lang.Exception
import kotlin.math.*

class PointF(val x: Float, val y:Float)

// ref.1: https://github.com/googlecodelabs/android-kotlin-drawing-canvas/
// ref.2: https://www.jianshu.com/p/ecbd0807e428
class CanvasView @JvmOverloads constructor(context: Context,
                 attrs: AttributeSet? = null,
                 defStyleAttr: Int = 0) : View(context,attrs,defStyleAttr) {
    // const string
    private val _BAIDU = "baidu"
    private val _DIDI = "didi"
    private val _HELLOBIKE = "hello_bike"
    private val _NIKERUN = "nike_run"
    private val _ALL = "all"

    // 曲线粗细
    private val _mSTROKEWIDTH = 8f
    // 坐标间隙, 取画板的 0.8 * 0.8
    private val mSPACE = 0.1f
    // 画曲线的笔
    private val paint = Paint().apply {
        color = Color.RED
        // Smooths out edges of what is drawn without affecting shape.
        isAntiAlias = true
        // Dithering affects how colors with higher-precision than the device are down-sampled.
        isDither = true

        style = Paint.Style.STROKE
        strokeJoin = Paint.Join.ROUND // default: MITER
        strokeCap = Paint.Cap.ROUND // default: BUTT
        strokeWidth = _mSTROKEWIDTH // default: Hairline-width (really thin)
    }
    // 画坐标点的笔
    private val pen = Paint().apply {
        alpha = 255
        style = Paint.Style.FILL
        color = Color.BLUE
    }
    // 需要画的路径
    private var path = Path()
    // 坐标点
    private lateinit var points : List<PointF>
    // 判断坐标的最大最小值
    private var minX = Float.MAX_VALUE
    private var maxX = -Float.MAX_VALUE
    private var minY = Float.MAX_VALUE
    private var maxY = -Float.MAX_VALUE
    // map from string -> Val
    private var minXMap = HashMap<String, Float>().apply {
        put(_ALL, Float.MAX_VALUE)
    }
    private var maxXMap = HashMap<String, Float>().apply {
        put(_ALL, -Float.MAX_VALUE)
    }
    private var minYMap = HashMap<String, Float>().apply {
        put(_ALL, Float.MAX_VALUE)
    }
    private var maxYMap = HashMap<String, Float>().apply {
        put(_ALL, -Float.MAX_VALUE)
    }

    // 原始点
    private lateinit var pointsBaidu: Array<PointF>
    private lateinit var pointsDidi: Array<PointF>
    private lateinit var pointsHelloBike: Array<PointF>
    private lateinit var pointsNikeRun: Array<PointF>

    // 动画
    private var valueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
        duration = 3000
        // no repeat
        repeatCount = 0
        // 插值器
        interpolator = AccelerateDecelerateInterpolator()
        addUpdateListener {
            println(it.animatedValue)
            invalidate()
        }
    }
    private var pathMeasure = PathMeasure()
    private var dst = Path()

    // 坐标点 flag
    private var touchFlag = false
    private var curX = 0f
    private var curY = 0f
    // For move
    private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
    // 写坐标的pen
    private val coordinatePen = Paint().apply {
        color = Color.BLACK
        textSize = 50f
    }
    // X 超过这部分则右对齐
    private val coordinateThre = 0.5f

    // 文件
    private val dir = File("${resources.getString(R.string.dir_name)}/images").apply {
        if (!(this.exists() && this.isDirectory)) {
            this.mkdir()
        }
    }
    // 背景
    private lateinit var bitmap: Bitmap
    // 图片信息
    private var center = PointF()
    private var expWidth = 0f
    private var expHeight = 0f
    private var zoom = 14
    // 获取原始点
    init {
        for (s in arrayOf(_BAIDU, _DIDI, _HELLOBIKE,_NIKERUN)) {
            val lines = File("${resources.getString(R.string.dir_name)}/dataset/${s}.txt").readLines()
            var txt = String()
            lines.forEach { line -> txt += line }
            txt = txt.replace(Regex("\\r|\\n"),"")

            val pos = txt.substring(1, txt.length-1)
                    .replace(Regex("\\[|\\]"),"")
                    .split(",")
                    .map {p -> p.toFloat()}

            when (s) {
                _BAIDU -> pointsBaidu = Array(pos.size/2) { id -> PointF(pos[id * 2], pos[id * 2 + 1]) }
                _DIDI -> pointsDidi = Array(pos.size/2) { id -> PointF(pos[id * 2], pos[id * 2 + 1]) }
                _HELLOBIKE -> pointsHelloBike = Array(pos.size/2) { id -> PointF(pos[id * 2], pos[id * 2 + 1]) }
                _NIKERUN -> pointsNikeRun = Array(pos.size/2) { id -> PointF(pos[id * 2], pos[id * 2 + 1]) }
                else -> pointsNikeRun = Array(pos.size/2) { id -> PointF(pos[id * 2], pos[id * 2 + 1]) }
            }

            genBound(s)
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // TODO: may be need something here
    }
    // draw
    override fun onDraw(canvas: Canvas)  {
        super.onDraw(canvas)

        // draw bitmap first
        if (::bitmap.isInitialized) {
            drawMap(canvas)
        }

        pathMeasure.setPath(path, false)
        dst.reset()
        val rate = (valueAnimator.animatedValue) as Float
        pathMeasure.getSegment(0f, rate*pathMeasure.length, dst, true)
        canvas.drawPath(dst, paint)

        // draw points
        if (::points.isInitialized) for (p in points) {
            canvas.drawCircle(p.x, p.y, 5f,pen)
        }
        // draw extra path
        if (valueAnimator.animatedValue == 1f) {
            paint.color = Color.CYAN

            canvas.drawPath(extraPath, paint)

            paint.color = Color.RED
        }
        // touch must on the top
        if (touchFlag) {
            drawTouch(canvas)
        }
    }
    // draw bitmap
    private fun drawMap(canvas: Canvas) {
        val posTL = (width * mSPACE).toInt()
        val posTT = (height * mSPACE).toInt()

        val posW = (width * (1-mSPACE )).toInt()
        val posH = (height * (1-mSPACE)).toInt()

        // draw canvas zoom automatically
        canvas.drawBitmap(bitmap,
                Rect(0,0,bitmap.width, bitmap.height),
                Rect(posTL,posTT,posW,posH),
                null
        )
    }
    // draw touch point
    private fun drawTouch(canvas: Canvas) {
        val rawPoint = transT(curX, curY)
        if (curX > width *  coordinateThre) {
            coordinatePen.textAlign = Paint.Align.RIGHT
        } else {
            coordinatePen.textAlign = Paint.Align.LEFT
        }
        canvas.drawText("${rawPoint.x}, ${rawPoint.y}",curX, curY, coordinatePen)
    }
    // touch Event
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> { curX = event.x; curY = event.y; touchStart() }
            MotionEvent.ACTION_UP -> { curX = event.x; curY = event.y; touchUp() }
            MotionEvent.ACTION_MOVE -> touchMove(event.x, event.y)
        }
        return true
    }
    private fun touchStart() {
        touchFlag = true
        invalidate()
    }
    private fun touchUp() {
        touchFlag = false
        invalidate()
    }
    private fun touchMove(eventX: Float, eventY: Float) {
        val dx = abs(eventX - curX)
        val dy = abs(eventY - curY)

        if (dx >= touchTolerance || dy >= touchTolerance) {
            curX = eventX
            curY = eventY
            invalidate()
        }
    }

    // public interface
    fun showPath(classname: String) {
        chmode(classname)
        // configure bitmap
        val filePath = getImages(classname)
        if (::bitmap.isInitialized) {
            bitmap.recycle()
        }
        bitmap = BitmapFactory.decodeFile(filePath)

        points = when (classname) {
            _BAIDU -> pointsBaidu
            _DIDI -> pointsDidi
            _HELLOBIKE -> pointsHelloBike
            _NIKERUN -> pointsNikeRun
            else -> pointsNikeRun
        }.map {trans(it.x, it.y)}

        // if show Path here
        // FixMe: here need to optimize , I think
        extraPath.reset()
        startDraw()
    }

    fun showPath(tpl: TupleList) {
        // 目前只有 ALL 走这个函数
        chmode(_ALL)

        val filePath = getImages(_ALL)
        if (::bitmap.isInitialized) {
            bitmap.recycle()
        }
        bitmap = BitmapFactory.decodeFile(filePath)

        val pnts = mutableListOf<PointF>()
        for (tp in tpl.tuplelist) {
            try {
                // ONLY X AND Y
                val x = tp.tuple[0].toString().toFloat()
                val y = tp.tuple[1].toString().toFloat()
                pnts.add(PointF(x,y))
            } catch (e: Exception) {
                e.printStackTrace()
                println("----------something Error!-----------")
                return
            }
        }
        points = pnts.map{trans(it.x,it.y)}

        // union 得到 的路径 会断
        extraPath.reset()
        detect()
        startDraw()
    }
    // 改变模式 即改变最大最小值
    // baidu map zoom level 14
    private val mZOOM = 14
    // for 1024*1024
    private val mBOUND = 1024
    // log dis ~= 0.15
    private val mLOGDIS = 0.15f
    // lat dis ~= 0.11
    private val mLATDIS = 0.11f
    // map zoom + 1 -> dis * 2
    private fun chmode(s : String) {
        val delLog = log2(mLOGDIS / (maxXMap[s]!! - minXMap[s]!!))
        val delLat = log2(mLATDIS / (maxYMap[s]!! - minYMap[s]!!))

        // zoom level
        val zoomLevel = min(delLat.toInt(), delLog.toInt())
        zoom = mZOOM + zoomLevel

        // expected width & height
        expWidth =  (mBOUND * ((1.shl(zoomLevel)) * (maxXMap[s]!! - minXMap[s]!!) / mLOGDIS))
        expHeight = (mBOUND * ((1.shl(zoomLevel)) * (maxYMap[s]!! - minYMap[s]!!) / mLATDIS))
        // bound to screen width & height
        if (expWidth * height > expHeight * width) {
            // using width as bound
            expHeight = expWidth * height / width
        } else {
            // using height as bound
            expWidth = expHeight * width / height
        }
        // calculate map info
        center.x = (maxXMap[s]!! + minXMap[s]!!) / 2
        center.y = (maxYMap[s]!! + minYMap[s]!!) / 2
        val xBound =  ( (expWidth / mBOUND) * (mLOGDIS / (1.shl(zoomLevel))) )
        val yBound =  ( (expHeight / mBOUND) * (mLATDIS / (1.shl(zoomLevel))))

        // change canvas bound
        minX = center.x - xBound / 2
        maxX = center.x + xBound / 2
        minY = center.y - yBound / 2
        maxY = center.y + yBound / 2

        getImages(s)
    }

    // 封装好控制点 和 动画

    private fun startDraw() {
        val controlList = genControlPoints(points)
        path.reset()
        path.moveTo(points[0].x, points[0].y)

        for (i in 0 until points.size-1) {
            val lftCtrl = controlList[i*2]
            val nxtCtrl = controlList[i*2+1]
            val nxt = points[i+1]
            path.cubicTo(lftCtrl.x, lftCtrl.y, nxtCtrl.x, nxtCtrl.y, nxt.x, nxt.y)
        }
        valueAnimator.start()
    }
    // detect & draw
    // define threshold
    private val mThre = 5 * 5
    private var extraPath = Path()
    private fun detect() {
        if (points.size <= 1) return

        var avg = distance(points[0], points[1])
        for (i in 1 until points.size-1) {
            val curDis = distance(points[i], points[i+1])
            if (curDis * i / avg >= mThre) {
                extraPath.moveTo(points[i].x, points[i].y)
                extraPath.lineTo(points[i+1].x, points[i+1].y)
            } else {
                avg += curDis
            }
        }
    }
    private fun distance(p1 :PointF, p2 :PointF) : Float{
        return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)
    }
    // 原始点 -> 坐标点
    private fun trans(x: Float, y: Float) : PointF {
        val newX = width * mSPACE + (x-minX) * genScaleX()
        val newY = height * (1 - mSPACE) - (y-minY) * genScaleY()
        return PointF(newX, newY)
    }
    // 坐标点 -> 原始点
    private fun transT(x: Float, y: Float): PointF {
        val rawX = (x - width * mSPACE) / genScaleX() + minX
        val rawY = (height * (1 -mSPACE) - y) / genScaleY() + minY
        return PointF(rawX, rawY)
    }
    // 单位X
    private fun genScaleX() :Float {
        return (width * (1 - 2 * mSPACE) ) / (maxX - minX);
    }
    // 单位Y
    private fun genScaleY() :Float {
        return (height * (1 - 2 * mSPACE)) / (maxY - minY)
    }
    // 封装的功能函数 —— 生成最大最小值
    private fun genBound(s:String) {
        val pnts = when (s) {
            _BAIDU -> pointsBaidu
            _DIDI -> pointsDidi
            _HELLOBIKE -> pointsHelloBike
            _NIKERUN -> pointsNikeRun
            else -> pointsNikeRun
        }

        var minX = Float.MAX_VALUE
        var maxX = -Float.MAX_VALUE
        var minY = Float.MAX_VALUE
        var maxY = -Float.MAX_VALUE

        pnts.forEach {
            minX = if (it.x < minX) it.x else minX
            maxX = if (it.x > maxX) it.x else maxX
            minY = if (it.y < minY) it.y else minY
            maxY = if (it.y > maxY) it.y else maxY
        }
        // put new value
        minXMap.put(s, minX)
        maxXMap.put(s, maxX)
        minYMap.put(s, minY)
        maxYMap.put(s, maxY)
        // update all
        minXMap[_ALL] = min(minXMap[_ALL]!!, minX)
        maxXMap[_ALL] = max(maxXMap[_ALL]!!, maxX)
        minYMap[_ALL] = min(minYMap[_ALL]!!, minY)
        maxYMap[_ALL] = max(maxYMap[_ALL]!!, maxY)
    }

    // ref 3: https://www.jianshu.com/p/c4601bab860a
    // 但我觉得应该修改成投影点, ↑
    private val mSMOOTH = 0.55f
    private fun genControlPoints(pnts: List<PointF>) : List<PointF>{
        val controlList = mutableListOf<PointF>()
        if (pnts.size <= 1) return controlList
        for ((i,pnt) in pnts.withIndex()) {
            when (i) {
                0 -> {
                    controlList.add(PointF(pnt.x + (pnts[i+1].x - pnt.x) * mSMOOTH, pnt.y))
                }
                pnts.size - 1 -> {
                    controlList.add(PointF(pnt.x + (pnt.x - pnts[i-1].x) * mSMOOTH, pnt.y))
                }
                else -> {
                    val nxt = pnts[i+1]
                    val lst = pnts[i-1]

                    // 切线 -->
                    val k = (nxt.y - lst.y) / (nxt.x - lst.x)
                    val b = pnt.y - k * pnt.x
                    val project = genProj(pnt, lst, nxt)
                    val lstCtrlX = pnt.x - (project.x - lst.x) * mSMOOTH
                    val lstCtrlY = k * lstCtrlX + b
                    val nxtCtrlX = pnt.x + (nxt.x - project.x) * mSMOOTH
                    val nxtCtrlY = k * nxtCtrlX + b

                    controlList.add(PointF(lstCtrlX, lstCtrlY))
                    controlList.add(PointF(nxtCtrlX, nxtCtrlY))
                }
            }
        }
        return controlList
    }
    // (x0,y0) 到 (x1,y1) (x2, y2) 的投影
    // Ax + By = C
    private fun genProj(target :PointF, start :PointF, end :PointF) :PointF {
        val A:Float
        val B:Float
        when {
            start.x == end.x -> {
                A = 1f
                B = 0f
            }
            start.y == end.y -> {
                A = 0f
                B = 1f
            }
            else -> {
                A = -1f
                B = (start.x - end.x) / (start.y - end.y)
            }
        }
        val C = A * start.x + B * start.y

        return PointF(
                (A*C + B*(B*target.x - A*target.y)) / (A*A + B*B),
                (B*C - A*(B*target.x - A*target.y)) / (A*A + B*B)
        )
    }

    // 获取图片
    // http 请求
    private lateinit var client: HttpClient
    private fun getImages(classname: String) : String{
        if (!::client.isInitialized) client = HttpClient()
        val filePath = "${dir.path}/${classname}_${expWidth.toInt()}_${expHeight.toInt()}_${zoom}.png"
        val file = File(filePath)
        if (file.exists()) return filePath;
        println(filePath)
        runBlocking {
            val httpResponse: HttpResponse = client.get("http://api.map.baidu.com/staticimage") {
                parameter("width", expWidth.toInt())
                parameter("height", expHeight.toInt())
                parameter("zoom", zoom)
                parameter("center", "${center.x},${center.y}")
            }
            val responseBody: ByteArray = httpResponse.body()
            file.writeBytes(responseBody)
            println("A file saved to ${file.path}")
        }
        return filePath
    }
}